Skip to content

feat(overview): 「近 7 天」脱离历史 200 条上限,并支持近 7/30 天 × 条数/字数/时长切换 - #909

Merged
H-Chris233 merged 7 commits into
Open-Less:betafrom
bigsongeth:feat/overview-period-metrics
Aug 5, 2026
Merged

feat(overview): 「近 7 天」脱离历史 200 条上限,并支持近 7/30 天 × 条数/字数/时长切换#909
H-Chris233 merged 7 commits into
Open-Less:betafrom
bigsongeth:feat/overview-period-metrics

Conversation

@bigsongeth

@bigsongeth bigsongeth commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

User description

问题:概览页的「近 7 天」会说谎

「近 7 天」柱状图从 list_history() 现算,而历史有 200 条硬上限。日均上百次听写的用户,两三天就把上周挤没了。

实测本机真实数据(history.json 正好卡在 200 条):

日期 近 7 天显示 年度活动实际
07-29 0 40
07-30 0 118
07-31 0 108
08-01 0 37
08-02 10 44
08-03 158 156
08-04 32 32

前四天全画成 0,而同一个页面上方的年度热力图那四天是亮的。同一页两块数据自相矛盾,用户没法判断该信哪个。

改动

1. 换数据源

「近 7 天」改读 activity 存储 —— 它保留两年、只存聚合数字、与历史保留策略解耦,本来就是年度热力图的数据源。改完之后柱子和格子同源,对得上。

2. activity 记录字数与时长

activity 此前每天只存一个条数。要支持「近 30 天说了多少字」,得先扩成 {count, chars, durationMs}。写入点手边就有 polishedraw.duration_ms,口径刻意与已有 UI 对齐:

  • chars = 最终插入文本的 Unicode 字符数,与历史详情页的「N 字」一致
  • durationMs = 录音时长,不含识别/润色耗时,与详情页「录音 x.x 秒」同源

磁盘格式向后兼容:老用户的 activity.json 全是裸数字({"2026-08-01": 5}),读不回来的话年度热力图会一次性清空,会被当成数据丢失。用 untagged enum 兼容,四个单测钉住契约:纯旧格式、纯新格式、新旧混排(升级当天必然出现)、缺字段的对象。

旧日期没有字数/时长,读回 0 —— 诚实缺省,好过整天丢掉条数。

存储代价可忽略:只存聚合数字、不存文本,一天一行,两年上限 731 行。

3. 周期 × 指标切换

卡片标题位变成「近 7 天 / 近 30 天」,右侧原来的「条数 / 天」变成「条数 / 字数 / 时长」。

卡片顶部显示周期总计(大字)+ 日均:想知道「这个月总共说了多少字」是要一个数,不是在 30 根柱子里目测求和。30 天模式下柱子只有几像素宽,逐柱数字会糊成一片,改为悬浮显示、横轴只标首/中/末三个日期。

4. ⌘R 刷新

历史页早就有这个键,概览页没有 —— 想看到新数据只能切到别的页再切回来触发重挂载。一次刷新本页三份数据(历史、活动、凭据),preventDefault 拦掉 webview 默认的整页 reload。

实现注记

  • 聚合逻辑抽成 lib/activityMetrics.ts 带单测:窗口长度恒定、缺失日期补 0、跨月边界、老数据无 chars 时不产生 NaN(NaN 会把柱状图的 max 算坏)
  • 日期键一律用本地年月日拼,不能用 toISOString() —— 后者按 UTC 切日,东八区凌晨的会话会算到前一天,与后端 chrono::Local 写的键对不上
  • 活动数据的 IPC 不再按 mobile 跳过。热力图在移动端仍不渲染(fix(android): 移动端隐藏概览页「年度活动」热力图 #861),但周期指标卡要渲染,跳过 IPC 会让它在移动端永远空

验证

cargo check / cargo test --lib / npm test / tsc --noEmit 全绿;浏览器里逐个切过两个周期 × 三个指标,核对总计与逐日数值;本地装机实测。

🤖 Generated with Claude Code


PR Type

Bug fix, Enhancement, Tests


Description

  • activity 存储新增每日字数与时长聚合

  • 近 7/30 天指标改用 activity 数据源

  • 支持条数/字数/时长切换与总计、日均

  • 兼容旧格式数据,新增测试与多语文案


Diagram Walkthrough

flowchart LR
  A["Daily dictation"] --> B["ActivityStore bump(count, chars, durationMs)"]
  B --> C["getActivityStats"]
  C --> D["Overview PeriodMetricsCard"]
  D --> E["7 / 30 days × count / chars / duration"]
Loading

File Walkthrough

Relevant files
Enhancement
7 files
history.rs
Extend activity stats response with chars and duration     
+9/-3     
dictation.rs
Record char count and duration on session end                       
+10/-6   
activity.rs
Expand activity store to aggregate chars and duration       
+133/-9 
types.rs
Add chars and duration fields to ActivityDay                         
+9/-1     
activityMetrics.ts
Add period series builder for activity metrics                     
+77/-0   
types.ts
Add optional chars and duration fields to ActivityDay       
+4/-0     
Overview.tsx
Replace week chart with switchable period metrics card     
+269/-63
Localization
5 files
en.ts
Add period and metric localization strings                             
+15/-2   
ja.ts
Add Japanese period and metric localization strings           
+15/-2   
ko.ts
Add Korean period and metric localization strings               
+15/-2   
zh-CN.ts
Add Simplified Chinese period and metric localization strings
+15/-2   
zh-TW.ts
Add Traditional Chinese period and metric localization strings
+15/-2   
Tests
2 files
activityMetrics.test.ts
Add tests for period series and legacy entries                     
+86/-0   
mock-data.ts
Update mock activity days with chars and duration               
+14/-1   

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

861 - Partially compliant

Compliant requirements:

  • 热力图组件在移动端仍不渲染(渲染条件保持移动端门控)。
  • 桌面端热力图渲染路径未改动,设置开关逻辑保留。

Non-compliant requirements:

  • 移动端仍然会调用 getActivityStats():本 PR 移除了原来的 if (mobile) return 守卫,并在每次挂载概览页时拉取活动数据。

Requires further human verification:

  • 需要在 Android / 窄屏真机上验证「年度活动」卡片确实不显示。
  • 需要人工验证移动端底边栏宽度与对齐、无横向溢出。
  • 需要人工验证桌面端设置开关仍能正常开/关热力图。
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Mobile IPC regression

The previous mobile guard that skipped getActivityStats() on Android / narrow viewports was removed, so every Overview mount now performs the activity IPC request and transfers the full daily-stats payload even when the heatmap is hidden. This contradicts ticket #861's acceptance criterion that mobile must not call getActivityStats(), and reintroduces the mobile WebView cost it was meant to avoid.

const [activity, setActivity] = useState<ActivityDay[] | null>(null);
const [activityError, setActivityError] = useState(false);
const refreshActivity = useCallback(() => {
  setActivityError(false);
  getActivityStats()
    .then(setActivity)
    .catch(error => {
      console.error('[overview] failed to load activity stats', error);
      setActivity(null);
      setActivityError(true);
    });
}, []);
useEffect(() => {
  refreshActivity();
}, [refreshActivity]);

bigsongeth and others added 7 commits August 5, 2026 23:30
activity.json 此前每天只存一个条数。概览页想给出「近 30 天说了多少字」
这类周期指标时,只能回头从 list_history() 现算 —— 而历史有 200 条硬上限,
日均上百次的用户两三天就把上周挤没了,算出来的字数与时长必然偏低。

把单日值从裸数字扩成 {count, chars, durationMs}:
- chars 口径 = 最终插入文本的 Unicode 字符数,与历史详情页的「N 字」一致;
- durationMs 口径 = 录音时长,不含识别/润色耗时,与详情页「录音 x.x 秒」同源。
两个口径都刻意跟已有 UI 对齐,避免同一个数在两处对不上。

磁盘格式用 untagged enum 兼容旧文件:老用户的 activity.json 全是裸数字,
读不回来的话年度热力图会一次性清空(会被当成数据丢失)。旧日期没有字数/
时长,读回 0 —— 诚实缺省,好过整天丢掉条数。四个单测钉住这个契约:纯旧
格式、纯新格式、新旧混排(升级当天必然出现)、以及缺字段的对象。

存储代价可忽略:只存聚合数字、不存文本,一天一行,两年上限 731 行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
两件事,同一张卡:

1. 修数据源。「近 7 天」此前从 list_history() 现算,而历史有 200 条硬上限。
   实测(日均约 88 次听写的真实数据):history.json 正好卡在 200 条,只装得下
   两天多,于是近 7 天里前 4 天全画成 0 —— 而同一页的年度热力图上那 4 天分别是
   40 / 118 / 108 / 37,亮着的。同一个页面两块数据自相矛盾。改读 activity 后
   两者同源,柱子和格子对得上。

2. 加周期与指标切换。卡片标题位变成「近 7 天 / 近 30 天」,右侧原来的「条数 /
   天」变成「条数 / 字数 / 时长」。月度字数是用户实际想看的数(跟按月计费的
   竞品比较时要用),此前完全看不到。

卡片顶部显示周期总计(大字)+ 日均:想知道「这个月总共说了多少字」是要一个数,
不是在 30 根柱子里目测求和。30 天模式下柱子只有几像素宽,逐柱数字会糊成一片,
改为 title 悬浮显示、横轴只标首/中/末三个日期。

聚合逻辑抽成 lib/activityMetrics.ts 并带单测:窗口长度恒定、缺失日期补 0、
跨月边界、老数据无 chars/durationMs 时不产生 NaN(NaN 会把柱状图的 max 算坏)。
日期键一律用本地年月日拼,不能用 toISOString() —— 后者按 UTC 切日,东八区凌晨
的会话会算到前一天,与后端 chrono::Local 写的键对不上。

顺带:活动数据的 IPC 不再按 mobile 跳过。热力图在移动端仍不渲染(issue Open-Less#861),
但周期指标卡是要渲染的,跳过 IPC 会让它在移动端永远空。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
历史页早就有这个键(History.tsx),概览页没有 —— 想看到新数据只能切到别的
页再切回来触发重挂载。⌘R 是「重新加载」的通用直觉,两个数据页应该一致。

一次刷新本页全部三份数据:历史、活动、凭据。preventDefault 拦掉 webview
默认的整页 reload,只重发 IPC,不让整个前端重挂载。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
finalText.length 按 UTF-16 码元计数,emoji / CJK 扩展 B 等增补平面字符
会被双算;后端 activity 的 chars 用 polished.chars().count()(Unicode 标量),
概览页「字数」指标也以此为准。改用 Array.from(...).length 后三处同口径。
概览页顶部「今日字数」此前用 finalText.length 按 UTF-16 码元计数,emoji / CJK 扩展 B 等增补平面字符会被双算;历史详情页(commit 70464a1)与后端 activity 的 chars 已是 Unicode 标量口径。抽成 lib/unicode.ts 的 countCodePoints 让两处前端共用,消除第三处口径漂移。
⌘R 一次触发历史、活动、凭据三份刷新,历史与活动此前没有 credentialsRequestSeq 那样的防竞态:快速连按可能让较早的慢响应后到并覆盖新数据。沿用同一模式,请求前自增序号、响应校验一致才 setState。
SegmentedToggle 是可点击按钮,与 _atoms 中 Btn 的 cursor: pointer 交互约定保持一致(此前为 default,看起来像不可点)。
@H-Chris233
H-Chris233 force-pushed the feat/overview-period-metrics branch from 70464a1 to 97b5461 Compare August 5, 2026 15:31
@H-Chris233
H-Chris233 merged commit 27198b9 into Open-Less:beta Aug 5, 2026
4 checks passed
bigsongeth added a commit to bigsongeth/openless that referenced this pull request Aug 5, 2026
beta 一次进了 13 个提交,其中三条正是本地 daily 上那批私货合上游了
(Open-Less#910 历史落点+重试、Open-Less#908 翻译、Open-Less#909 概览),daily 的 patch queue 相应缩小。

冲突只有 selection.rs 一处,而且 HEAD 侧是空的:beta 的选区润色 macOS 移植
(Open-Less#926)在同一位置加了 current_front_app_pid 与 Windows/其他平台的
current_front_app,本分支在附近加了 bundle_id_for_pid(闸门按元素归属判定用)。
两者互不相干,取 beta 侧即可。

解完核对:bundle_id_for_pid 仍在(1 处),current_front_app 三个定义分属
macOS / Windows / 其他平台且 cfg 互斥,current_front_app_pid 无重复。

验证:cargo check 干净、cargo test --lib 1032 passed(beta 带来 30 个新用例)、
tsc --noEmit 干净、npm test 通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants